// Copyright (C) 1995 Steve Hayman
// Use is governed by the MiscKit license

#import <misckit/MiscShell.h>

/*
 * $Header: /SAHayman/LocalDeveloper/Source/MiscKit/Palettes/MiscShell/MiscShell.subproj/RCS/MiscShell.m,v 1.2 94/08/04 17:39:39 shayman Exp Locker: shayman $
 * $Log:	MiscShell.m,v $
 * Revision 1.2  94/08/04  17:39:39  shayman
 * Fiddling with string values
 * 
 */

/*
 * Note:
 * In several spots we do things like
 *   if( [aVar isKindOfClassNamed:"DBTableView"] )
 * instead of
 *   if ( [avar isKindOf:[DBTableView class]] )
 *
 * This is because you can do the first test without having to link
 * DBKit into your program.  Doing the second requires linking DBKit,
 * including the DBKit header files in this program, etc etc, which
 * we'd rather avoid if we can.  Same for NXTableView.  Since I want this
 * object to be usable with either, this seems like the best way to
 * avoid various linking problems.
 *
 * I have done the same thing with other AppKit classes, e.g.
 *  if ( [aVar isKindOfClassNamed:"Matrix"] )
 * for consistency.
 */

// To avoid warnings, the following "category" defines some methods:
// Added 1/17/95, DAY
@interface Object(MiscShell_Warning_Suppressor)
- setDataSource:aSource;
- (unsigned int)columnCount;
- columnAt:(unsigned int)aPosition;
- identifier;
- setIdentifier:anIdentifier;
- reloadData:sender;
@end




#define MISC_SHELL_VERSION 3

@interface MiscShell(PrivateMethods)
- setScriptFromCString:(const char *)str;
- sendText:(const char *)buffer andNewline:(BOOL)newline to:text;
- setupEnvironment:process forSender:sender;
- addEnvVar:(const char *)varName value:(const char *)val to:env;
- handleCompleteLine;
- startOutput;
- finishOutput;
- resort:(int)keyField;

- field:(int)n ofString:s;
- splitupCurrentLine;
- (BOOL)useCustomDelimiters;
- (int) fieldsInString:(MiscString *)str;

@end

@implementation MiscShell(PrivateMethods)

- (BOOL)useCustomDelimiters
{
    return [[self customDelimiters] length] > 0;
}
/*
 * Add a bunch of environment variables to a process.
 */
- setupEnvironment:proc forSender:sender
{
    NXBundle *mainBundle = [NXBundle mainBundle];
    id env = [proc environment];	// a MiscStringArray

    int i;
    char varName[3];
    id v;
    
    /*
     * Go through each of our v1...v9 instance variables in turn by
     * making use of this clever object_getInstanceVariable runtime function.
     */
    for ( i = 1; i <= 9; i++ ) {
	sprintf(varName, "v%d", i);
	// See if we have an instance variable by that name ..
	if ( object_getInstanceVariable(self, varName, (void *)&v) ) {
	
	    // Now see if that obj responds to stringValue:, and if so,
	    // add an environment var representing its current stringValue.
	    
	    // Special case: if the object is a browser, we write
	    // out the value of all its selected cells, separated
	    // by tabs.  Rather than just the stringValue.
	    
	    if ( [v isKindOfClassNamed:"NXBrowser"] ) {
		List *selections = [[List alloc] init];
		MiscString *str = [[MiscString alloc] init];
		int i;
		id aCell;
		
		[v getSelectedCells:selections];
		
		i = 0;
		while ( aCell = [selections objectAt:i++] ) {
		    [str catFromFormat:"%s", [aCell stringValue]];
		    
		    if ( i < [selections count] )
			[str catFromFormat:"\t"];
		}
		
		[self addEnvVar:varName value:[str stringValue] to:env];
		[selections free];
		[str free];
		    
	    } else if ( [v respondsTo:@selector(stringValue)] )
		[self addEnvVar:varName value:[v stringValue] to:env];
	}
    }

    
    // Add a var called "sender" containing the sender's string value
    // (if known.)
    
    // TODO - should we be using [sender selectedCell] here if it's
    // a Matrix sending us this message?
    
    
    if ( [sender respondsTo:@selector(stringValue)] ) 
	[self addEnvVar:"sender" value:(const char *)[sender stringValue] 
	    to:env];

    // What the hell, add a var called "senderTag" too.
    {
	char tag[10];
	if ( [sender respondsTo:@selector(tag)] ) {
	    sprintf(tag, "%d", [sender tag]);
	    [self addEnvVar:"senderTag" value:tag to:env];
	}
    }
    
    // TODO
    // Maybe if it's a Form sending us the message, we add a bunch
    // of variables for each entry in the Form.
    
    // Add a var called "mainBundle" giving our main application
    // directory.
    
    [self addEnvVar:"mainBundle" value:[mainBundle directory]
	to:env];
	
    

    return self;
}
// Add an environment variable to a MiscStringArray, which belongs to
// the subprocess

- addEnvVar:(const char *)varName value:(const char *)value to:env
{
    MiscString *newStr = [[MiscString alloc] init];
    if ( ! value )
	return nil;
    [newStr catFromFormat:"%s=%s", varName, value];
    [env addString:[newStr stringValue]];
    [newStr free];
    return self;
}

// Internal method, set our script from a (char *)

- setScriptFromCString:(const char *)str
{
    if ( script )
	[script free];
    script = [MiscString newWithString:str];
    return self;
}
// Internal method that passes some text along to either a
// scrolling text object (appended), or something responding to setStringValue.
// it's appended to the end.  
// The nl variable controls whether we send a newline or not.
// If true, and if the destination object is a Text object, we append
// a newline.

- sendText:(const char *)buffer andNewline:(BOOL) nl to:anObject
{
    int len;
    
    if ( !anObject )
	return nil;
    
    if ( [anObject respondsTo:@selector(setStringValue:)] ) {
	return [anObject setStringValue:buffer];
    }
    
    // Otherwise it should be a text object.
    if ( ! [anObject isKindOfClassNamed:"Text"] ) {
	return nil;
    }
    len = [anObject textLength];
    [anObject setSel:len:len];
    [anObject replaceSel:buffer];
    
    if ( nl ) {
	len = [anObject textLength];
	[anObject setSel:len:len];
	[anObject replaceSel:"\n"];
    }

    [anObject scrollSelToVisible];
    
    return self;
}

/*
 * just read a line that looks like
 * ALERT;Are you sure?;Yes;No
 * Do an NXRunAlertPanel and write back the result as an integer.
 */
- doAlert
{
    MiscString *message, *b1, *b2, *b3;
    int r;
    char rbuf[10];

    message = [currentLine extractPart:1 useAsDelimiter: ';'];
    b1 = [currentLine extractPart:2 useAsDelimiter: ';'];
    b2 = [currentLine extractPart:3 useAsDelimiter: ';'];
    b3 = [currentLine extractPart:4 useAsDelimiter: ';'];
    
    r = NXRunAlertPanel([NXApp appName], 
	[message stringValue],
	[b1 stringValue], [b2 stringValue], [b3 stringValue] );
	
    sprintf(rbuf, "%d", r);
    [process send:rbuf withNewline:YES];
    
    [message free];
    [b1 free];
    [b2 free];
    [b3 free];
    
    return self;
}

- doOpen
{
    id op = [OpenPanel new];
    if ( [op runModalForDirectory:NULL file:NULL] ) {
	[process send: [op filename] withNewline:YES];
    } else {
	[process send:"\n" withNewline:NO];
    }
    return self;
}
/*
 * Do whatever is appropriate upon reading a complete line from the shell.
 */
- handleCompleteLine
{

    /*
     * Is it one of our special strings?
     */
    /*
     * Run special magic commands that write back answers when
     * we see certain strings in the output.
     */
    MiscString *var, *value;
    id varObj;
    // ALERT;Are you sure?;Yes;No
    if ( [currentLine cmp:"ALERT" n:5] == 0 ) {
	return [self doAlert];
    } else if ( [currentLine cmp:"OPEN" n:4] == 0 ) {
	return [self doOpen];
    }
    
    
    if ( [currentLine matchesRegex:"^v[0123456789]=" ] ) {
	var = [currentLine extractPart:0 useAsDelimiter:'='];
	value = [currentLine extractPart:1 useAsDelimiter:'='];
	
	// ooh tricky runtime stuff - get a pointer to the
	// instance var whose name is in "var"
	if ( object_getInstanceVariable(self, [var stringValue], (void *)&varObj) ) {
	    if ( [varObj respondsTo:@selector(setStringValue:)] ) {
		[varObj setStringValue:[value stringValue]];
	    }
	}
	[var free];
	[value free];
	return self;
    }
	
	
    /*
     * Add the line to our lines array.
     */
    [lines addString: [currentLine stringValue]];
    
    /*
     * Create a MiscStringArray out of it by splitting it into
     * fields, and add it to the linesBrokenIntoFields List.
     */
    
    [self splitupCurrentLine];
    
    /*
     * Send the target/action message
     */
    if ( _target && action && [_target respondsTo:action] ) {
	[_target perform:action with:self];
    }
    
    /*
     * Send the line - and a newline - to standard output.
     */
    [self sendText:[currentLine stringValue] andNewline:YES to:standardOutput];
    
    /*
     * If standard out is a browser, add a row to it.
     */
    if ( [standardOutput isKindOfClassNamed:"NXBrowser"] ) {
	id m = [standardOutput matrixInColumn:0];
	id newCell;
	
	[m renewRows: [lines count] cols:1];
	newCell = [[m cellList] lastObject];
	
	
	[newCell setStringValue:[currentLine stringValue]];
	[newCell setLeaf:YES];
	[newCell setLoaded:YES];
	[m sizeToCells];
	[standardOutput sizeToFit];
    } else if ( [standardOutput isKindOfClassNamed:"DBTableView"]
	    ||  [standardOutput isKindOfClassNamed:"NXTableView"]) {
	;	// Don't need to do anything here, all handled in finishOutput
		// for now.
    } else if ( [standardOutput isKindOfClassNamed:"Matrix"] ) {
	[standardOutput selectCellWithTag:[self intValue]];
    } else if ( [standardOutput isKindOfClassNamed:"MiscShell"] ) {
	/*
	 * Ask the other process to read our string value and
	 * place it on its stdin.  This is a way that you can
	 * pipeline two shell objects.
	 */
	[standardOutput takeStdinFrom:self];
    }
    return self;
}
- (int) fieldsInString:(MiscString *)str
{
    
    if ( [self useCustomDelimiters] ) {
	// n delimiters -> n+1 fields
	return [customDelimiters numWords] + 1;
    } else if ( [self delimiter]  )
	/*
	 * count delimiters.
	 * one delimiter = two fields.
	 */
	return [str numOfChar:[self delimiter]] + 1;
    else	
	/*
	 * delimiter is 0 - count words delimited by whitespace
	 */
    	return [str numWords];

}	


- splitupCurrentLine
{
    int i;
    MiscStringArray *a = [[MiscStringArray alloc] init];
    MiscString *curLine = [[lines strings] lastObject];
    
    // Extract each field in turn, add it to the array for this line
    for ( i = 0; i < [self fieldsInString:curLine]; i++ ) {
	[a addString:  [[self field:i ofString:curLine] stringValue]];
    }
    
    [linesBrokenIntoFields addObject:a];
    return self;
}
	
/*
 * This is here so that we can do clever things if our standardOutput
 * is a table view object.  This method is automatically called when
 * the outlet is initialized.
 * We need to tell the table view that we are its data source, and
 * we need to set identifiers for each of its columns.  Although those
 * identifiers are normally objects, it's apparently ok to use
 * other 32-bit quantities such as integers, so we'll
 * just set the identifier of column "i" to be "i".
 *
 * TODO - is it legal to be doing this here?  is the table view set up
 * properly when setStandardOutput is called?
 */
- setStandardOutput:newOutput
{
    int i;
    standardOutput = newOutput;
    if ([standardOutput isKindOfClassNamed:"DBTableView"]
	   || [standardOutput isKindOfClassNamed:"NXTableView"] ) {
	
	[standardOutput setDataSource:self];
	/*
	 * Become its delegate so we get column moved messages
	 */
	[standardOutput setDelegate:self];
	/*
	 * Put numeric identifiers on each of its columns.
	 */
	for ( i = 0; i < [standardOutput columnCount]; i++ ) 
	    [[standardOutput columnAt:i] setIdentifier:(void *)i];
    }
    
    return self;
}

/*
 * startOutput does any special initialization of certain kinds of
 * output objects.
 */

- startOutput
{
    return self;
}

/*
 * finishOutput is called after the subprocess has executed, and can
 * be used to do any sort of output display cleanup.
 */
- finishOutput
{
    if ([standardOutput isKindOfClassNamed:"DBTableView"]
	|| [standardOutput isKindOfClassNamed:"NXTableView"]) {
	[standardOutput reloadData:self];
    } else  if ( [standardOutput isKindOfClassNamed:"NXBrowser"] ) {
	[standardOutput display];
    }
    return self;
}

/*
 * Here is a shellsort function, from Kernighan & Ritchie, page 116,
 * modified to pass a 3rd bonus parameter to the comparison routine.
/*
 * Resort the output lines based on the value of column n.
 * We do this by creating a ListSortedByFields which duplicates
 * the existing string list then we tell the ListSortedByFields to
 * sort itself.
 *
 * Multiplying by sortWhenColumnsMove does the right thing for
 * ascending vs descending sorts.
 */

- (int)fieldComp:(int)n forLines:(int)a :(int)b
{
    id field1 = [self line:a field:n];
    id field2 = [self line:b field:n];
    
        
    int rval;
    // If field1 looks like a number, compare numerically.
    // TODO - do the right thing if it looks like
    // a time value (HH:MM)
        
    if ( [field1 matchesRegex:":[0-9][0-9]$" ] ) {
	// Replace the ":" with a ".", which will
	// make the comparison work just as if it was a float.
	[field1 replace: ":" with: "."];	
	[field2 replace: ":" with: "."];
    }
    
    if ( [field1 matchesRegex:"^[0-9]"] ) {
        double n1, n2;

	n1 = atof([field1 stringValue]);
	n2 = atof( [field2 stringValue] );
	
	// Check for a K or M suffix, multiply appropriately.
	// This is so I can sort the output of "ps" nicely.
	if ( [field1 endcmp:"M"] == 0 )
	    n1 *= 1024 * 1024;
	if ( [field1 endcmp:"K"] == 0 )
	    n1 *= 1024;
	if ( [field2 endcmp:"M"] == 0)
	    n2 *= 1024 * 1024;
	if ( [field2 endcmp:"K"] == 0)
	    n2 *= 1024;
	    
	
	rval = (n1 < n2) ? -1 : 
		    (n1 > n2) ? 1 : 0;
	    
    } else {
	/*
	 * Do a regular string comparison.
	 */
	rval = [field1 compareTo: field2];
    }
    
    return rval * [self sortWhenColumnsMove];
}

- resort:(int)fieldNumber
{
    List * strings = [lines strings];
    id a, b;
    int c, d, stride;
    BOOL found;
    int n = [strings count];
    /*
     * ShellSort, from the SortingInAction miniexample
     */
#define STRIDE_FACTOR 3
    stride = 1;
    while ( stride <= n )
	stride = stride * STRIDE_FACTOR + 1;

    while ( stride > (STRIDE_FACTOR - 1)) {
	stride = stride / STRIDE_FACTOR;
	for ( c = stride; c < n; c++ ) {
	    found = NO;
	    d = c - stride;
	    while ( (d >= 0) && !found ) {

		if ( [self fieldComp:fieldNumber forLines:d:d+stride] > 0 ) {
		    // Swap the "lines" array ...
		    a = [strings objectAt:d];
		    b = [strings objectAt:d+stride];
		    [strings replaceObjectAt:d with:b];
		    [strings replaceObjectAt:d+stride with:a];
		    
		    
		    //and the linesBrokenIntoFields list
		    a = [linesBrokenIntoFields objectAt:d];
		    b = [linesBrokenIntoFields objectAt:d+stride];
		    [linesBrokenIntoFields replaceObjectAt:d with:b];
		    [linesBrokenIntoFields replaceObjectAt:d+stride with:a];

		    d -= stride;
		} else
		    found = YES;
	    }
	}
    }

    return self;
}

/*
 * Utility routine to return a particular field of a particular string,
 * using our delimiter.  If delimiter is 0, we look for blank-separated words;
 * otherwise we look for the particular delimited field.
 */
- field:(int)f ofString:s
{
    id thisField = nil;
    int startPos, endPos;
    MiscString *delim;
    
    if ( [self useCustomDelimiters] ) {
	// customDelimiters is a string like this
	// 0-4 7-9 13-22
	// that defines the boundaries of each field.  So, in this
	// case, if we want field 2, we extract chars 13-22.
	
	delim = [[self customDelimiters] wordNum:f];
	
	// delim is now something like "5-9", extract the
	// starting and ending positions.  If it's "72-", that
	// means "72 to end of line"
	
	if ( [delim stringValue] && (sscanf( [delim stringValue], "%d-%d", &startPos, &endPos) == 2) ) {	
	    thisField = [s midFrom:startPos to:endPos];
	} else if ( [delim stringValue ] && (sscanf( [delim stringValue], "%d-", &startPos) == 1) ) {
	    thisField = [s midFrom:startPos to: [s length]];
	}
	[delim free];
    } else if ( [self delimiter] )
	thisField = [s extractPart:f 
	    useAsDelimiter:[self delimiter]];
    else {
	/*
	 * take the column'th word.
	 * Current bug in MiscString: wordNum:n for n > numWords returns
	 * the last word rather than nil.  So we check that.
	 */
	/* if ( f < [s numWords] )
	    thisField = [s wordNum:f];
	else
	    thisField = nil;
	*/ /* I fixed the MiscString bug, so I'm taking this out. -don */
    thisField = [s wordNum:f];
    }

    return thisField;
}

@end

    
@implementation MiscShell(TableViewDelegate)
- (unsigned int) rowCount 
{
    return [self lineCount];
}

- (unsigned int) columnCount
{
    return 1;	// ??? todo - what do I put here? does it matter?
}

/*
 * This is a table view asking for the value at row aPosition,
 * column identifier.
 */
- getValueFor:identifier at:(unsigned int)aPosition into:aValue
{    
    [aValue setStringValue: 
	[[self line:aPosition field:(int)identifier] stringValue] ];
    return self;
}

/*
 * If you're the delegate of a DBTableView, you get these messages
 * when the columns are resized.  Not sure just how I want to deal 
 * with this yet.
 *
 * Current Plan - check the sortWhenColumnsMove variable.
 * If 0, do nothing.
 * If 1, resort ascending
 * If -1, resort descending
 */ 
- tableView:sender movedColumnFrom:(unsigned int) old to:(unsigned int) new
{
    
    /*
     * Resort based on the identifier of the new first column.
     */
    if ( [self sortWhenColumnsMove] ) {
	[self resort: (int)[[sender columnAt:0] identifier]];
	[sender reloadData:self];
    }
    return self;
} 

@end
	
@implementation MiscShell

+ initialize
{
    if (self == [MiscShell class]) {
	/*
	* **** Archiving: READ ME **** After bumping the _VERSION, it is
	* considered common practice to add a comment line indicating the new
	* version number, date, and modifier. Optionally, the reason for the
	* change. There is no need to modify the setVersion message. BJM
	* 5/24/94 
	*/
	// version 0: initial.  (sah)
	// version 1: adds customDelimiters var.  (sah, sep 13 1994)
	// version 2: adds linesBrokenIntoFields array (sah, sep 14 1994)
	// version 3: fixes a bug with archiving BOOL vars (sah, jan 4 1995)
	[[MiscShell class] setVersion:MISC_SHELL_VERSION];
    }
    
    return self;
}

- init
{
    self = [super init];
    script = [[MiscString alloc] init];
    fullOutput = [[MiscString alloc] init];
    currentLine = [[MiscString alloc] init];
    lines = [[MiscStringArray alloc] init];
    
    [self setDelimiter:0];		// means "parse words"
    [self setRunToCompletion:NO];	// run asynchronously
    [self setSortWhenColumnsMove:NO];
    
    linesBrokenIntoFields = [[List alloc] init];
    return self;
}

/*
 * Initialize, and run a non-interactive command and wait for it to terminate.
 */
- initWithCommand:(const char *)cmd
{
    [self init];
    [self setRunToCompletion:YES];
    [self setScriptFromCString:cmd];
    [self executeScript:self];
    
    return self;
}
    

- free
{
    if ( script )
	script = [script free];
    if ( process ) {
	[process terminate:self];
	process = [process free];
    }
    if ( fullOutput )
	fullOutput = [fullOutput free];
    [currentLine free];	
    return [super free];
}

// Archiving methods

- read:(NXTypedStream *)stream
{
    int version;
    Class myClass = [self class];
    int int1, int2;	// compensate for old read/write bugs
    
    [super read:stream];
    version = NXTypedStreamClassVersion(stream, "MiscShell");
    
    switch (version) {
    case MISC_SHELL_VERSION: {
		/*
		 * Version 3 correctly reads/writes BOOL vars as "c", not "i"
		 */
		standardOutput = NXReadObject(stream);
		standardInput = NXReadObject(stream);
		standardError = NXReadObject(stream);
		v1 = NXReadObject(stream);
		v2 = NXReadObject(stream);
		v3 = NXReadObject(stream);
		v4 = NXReadObject(stream);
		script = NXReadObject( stream );
		_target = NXReadObject( stream );
		NXReadTypes(stream, ":", &action);
		NXReadTypes(stream, "@", &process);
		NXReadTypes(stream, "@", &fullOutput);
		NXReadTypes(stream, "c",  &executionInProgress);
		NXReadTypes(stream, "@",   &currentLine);
		NXReadTypes(stream, "cc", &currentLineIsComplete, &runToCompletion);
		NXReadTypes(stream, "@",   &lines);
		NXReadTypes(stream, "c", &delimiter);
		NXReadTypes(stream, "c", &sortWhenColumnsMove);
		NXReadTypes(stream, "@", &customDelimiters);
		NXReadTypes(stream, "@", &linesBrokenIntoFields);	// new for v2
		break;
	}
    case 2: {
		/*
		 * Version 2 adds the linesBrokenIntoFields var
		 */
		standardOutput = NXReadObject(stream);
		standardInput = NXReadObject(stream);
		standardError = NXReadObject(stream);
		v1 = NXReadObject(stream);
		v2 = NXReadObject(stream);
		v3 = NXReadObject(stream);
		v4 = NXReadObject(stream);
		script = NXReadObject( stream );
		_target = NXReadObject( stream );
		NXReadTypes(stream, ":", &action);
		NXReadTypes(stream, "@", &process);
		NXReadTypes(stream, "@", &fullOutput);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "i",  &int1); executionInProgress = int1;
		NXReadTypes(stream, "@",   &currentLine);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "ii", &int1, &int2);
				currentLineIsComplete = int1;
				runToCompletion = int2;
		NXReadTypes(stream, "@",   &lines);
		NXReadTypes(stream, "c", &delimiter);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "i",  &int1); sortWhenColumnsMove = int1;
		NXReadTypes(stream, "@", &customDelimiters);
		NXReadTypes(stream, "@", &linesBrokenIntoFields);	// new for v2
		break;
	}
    case 1: {
		/*
		 * Version 1 adds a customDelimiters instance var
		 */
		standardOutput = NXReadObject(stream);
		standardInput = NXReadObject(stream);
		standardError = NXReadObject(stream);
		v1 = NXReadObject(stream);
		v2 = NXReadObject(stream);
		v3 = NXReadObject(stream);
		v4 = NXReadObject(stream);
		script = NXReadObject( stream );
		_target = NXReadObject( stream );
		NXReadTypes(stream, ":", &action);
		NXReadTypes(stream, "@", &process);
		NXReadTypes(stream, "@", &fullOutput);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "i",  &int1); executionInProgress = int1;
		NXReadTypes(stream, "@",   &currentLine);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "ii", &int1, &int2);
				currentLineIsComplete = int1;
				runToCompletion = int2;
		NXReadTypes(stream, "@",   &lines);
		NXReadTypes(stream, "c", &delimiter);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "i",  &int1); sortWhenColumnsMove = int1;
		NXReadTypes(stream, "@", &customDelimiters);
		linesBrokenIntoFields = [[List alloc] init];	// new - need one
		break;
	}
    case 0: {
		standardOutput = NXReadObject(stream);
		standardInput = NXReadObject(stream);
		standardError = NXReadObject(stream);
		v1 = NXReadObject(stream);
		v2 = NXReadObject(stream);
		v3 = NXReadObject(stream);
		v4 = NXReadObject(stream);
		script = NXReadObject( stream );
		_target = NXReadObject( stream );
		NXReadTypes(stream, ":", &action);
		NXReadTypes(stream, "@", &process);
		NXReadTypes(stream, "@", &fullOutput);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "i",  &int1); executionInProgress = int1;
		NXReadTypes(stream, "@",   &currentLine);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "ii", &int1, &int2);
				currentLineIsComplete = int1;
				runToCompletion = int2;
		NXReadTypes(stream, "@",   &lines);
		NXReadTypes(stream, "c", &delimiter);
		// Written as int in versions <= 2, but really a char
		NXReadTypes(stream, "i",  &int1); sortWhenColumnsMove = int1;
		linesBrokenIntoFields = [[List alloc] init];	// new - need one
		break;
	}
    default: {
		NXLogError("[%s %s] - unknown version of %s in typed stream",
				[myClass name], sel_getName(_cmd), 
				[myClass name]);
		break;
	}
    }
    return self;
}

- write:(NXTypedStream *)stream
{
    [super write:stream];
    
	NXWriteObjectReference( stream, standardOutput );
	NXWriteObjectReference( stream, standardInput );
	NXWriteObjectReference( stream, standardError );
	NXWriteObjectReference( stream, v1 );
	NXWriteObjectReference( stream, v2 );
	NXWriteObjectReference( stream, v3 );
	NXWriteObjectReference( stream, v4 );
	NXWriteObject( stream, script );
	NXWriteObjectReference( stream, _target );
	NXWriteTypes(stream, ":",  &action);

	NXWriteTypes(stream, "@",  &process);
	NXWriteTypes(stream, "@",  &fullOutput);
	NXWriteTypes(stream, "c",  &executionInProgress);	// BOOL
	NXWriteTypes(stream, "@",  &currentLine);
	NXWriteTypes(stream, "cc", &currentLineIsComplete, &runToCompletion); // BOOL
	NXWriteTypes(stream, "@",   &lines);
	NXWriteTypes(stream, "c", &delimiter);
	NXWriteTypes(stream, "c", &sortWhenColumnsMove);
	NXWriteTypes(stream, "@", &customDelimiters);	// added in v1
	NXWriteTypes(stream, "@", &linesBrokenIntoFields);	// added in v2

    return self;
}

- target { return _target; }
- setTarget:aTarget
{
    _target = aTarget;
    return self;
}
- (SEL)action { return action; }
- setAction:(SEL)anAction
{
    action = anAction;
    return self;
}

/*
 * Our "string value" is the "current" line we've received 
 * (which doesn't contain a newline.)
 */
 
- (const char *)stringValue 
{
    return [currentLine stringValue];
}

/*
 * Int, double, float values are just derived from our string value.
 * note: no error checking as to whether the string value really is
 * a number.
 */

- (double) doubleValue
{
    return ( atof([self stringValue]) );
}

- (float) floatValue
{
    return ( (float) atof([self stringValue]) );
}
- (int) intValue
{
    return ( atoi([self stringValue]) );
}


/*
 * Set and retrieve the actual script.
 * We store it internally as a MiscString
 */
 
- (MiscString *)script
{
    return script;
}

// Inspector sends this when the script changes
- setScript:(MiscString *)newScript
{
    return [self setScriptFromCString: [newScript stringValue]];
}
- (MiscString *)customDelimiters
{
    return customDelimiters;
}

// Inspector sends this when the script changes
- setCustomDelimiters:(MiscString *)d
{
    [customDelimiters free];
    customDelimiters = [d copy];
    return self;
}

// Messages from Controls

- executeScript:sender
{

    if ( executionInProgress ) {
	NXBeep();
	return nil;
    }
    
    if ( process ) {
	[process terminate:self];
	[process free];
    }
    
    /*
     * Get rid of old accumulated output.
     */
    
    [fullOutput setStringValue:""];
    [currentLine setStringValue:""];
    currentLineIsComplete = NO;
    [[lines strings] freeObjects];
    
    [linesBrokenIntoFields freeObjects];
    
    /*
     * Here we have special pre-output checks for certain kinds of
     * output objects.
     */
    [self startOutput];
    
    // Create a subprocess to execute the script.  Don't run it just yet.
    
    process = [[MiscSubprocess alloc] init:NULL withDelegate:self];
    
    
    // Set up subprocess environment here - a bunch of
    // environment variables that tell the process about
    // the values of v1, v2, etc.
    
    [self setupEnvironment:process forSender:sender];
    
    // And finally start the process going.  
        
    [process execute:[script stringValue] 
	withPtys:NO
	asynchronously: ![self runToCompletion]];
            	   
    return self;
}

/*
 * These messages might arrive from either a control or a matrix of
 * controls. 
 */
 
- executeFromStringValue:sender
{
    if ( [sender isKindOfClassNamed:"Matrix"] )
	sender = [sender selectedCell];
	
    [self setScriptFromCString:[sender stringValue]];
    return [self executeScript:sender];
}
- executeFromTitle:sender
{
    if ( [sender isKindOfClassNamed:"Matrix"] )
	sender = [sender selectedCell];

    [self setScriptFromCString:[sender title]];
    return [self executeScript:sender];
}

- executeFromAltTitle:sender
{
    if ( [sender isKindOfClassNamed:"Matrix"] )
	sender = [sender selectedCell];

    [self setScriptFromCString:[sender altTitle]];
    return [self executeScript:sender];
}

- pause:sender
{
    return [process pause:sender];
}
- resume:sender
{
    return [process resume:sender];
}

- terminate:sender
{
    return [process terminate:sender];
}

/*
 * Methods that return particular lines, or fields within lines.
 */

- (int)lineCount {
    return [lines count];
}

- (int) fieldsInLine:(int)n
{
    return [[linesBrokenIntoFields objectAt:n] count];
}

- (MiscStringArray *)lines { return lines; }

/*
 * Return a copy of the MiscString that holds line number n.
 * We return a copy for consistency with the line:field: method, so that*
 * the caller is responsible for freeing both.
 * Just returning [[lines strings] objectAt:n] would hand back a string
 * owned by the MiscStringArray object, which the caller shouldn't free.
 *
 * Boy it will be nice when libFoundation_s.a is available everywhere
 * and we can do this more rationally.
 */
- (MiscString *)line:(int)n
{
    id str = [[lines strings] objectAt:n];
    if ( str )
	return [str copy];
    else
	return nil;
}

/*
 * Return the MiscString representing field f of line n;
 * the caller should NOT free it.
 */
- (MiscString *)line:(int)n field:(int)f
{
    return [[[linesBrokenIntoFields objectAt:n] strings] objectAt:f];
}



// MiscSubprocess delegate methods
-  subprocess:sender output:(const char *)buffer
{
    
    /*
     * fullOutput records the entire output of the script, so add
     * the buffer to the end.
     */
    [fullOutput cat:buffer];

    /*
     * Now.  We have to decide how this new chunk of data, with
     * possibly embedded newlines, affects the current line.  We want
     * to fire off a target/action message every time we receive a newline.
     */
    
	
    while ( *buffer ) {
    
	/*
	 * If we have previously accumulated a complete newline, it's
	 * no longer complete.
	 */
	if( currentLineIsComplete ) {
	    [currentLine setStringValue:""];
	    currentLineIsComplete = NO;
	}

	/*
	 * If we are looking at a newline, then the current output
	 * line is complete, so fire the target/action message.
	 */
	if ( *buffer == '\n' ) {
	    
	    [self handleCompleteLine];
	    currentLineIsComplete = YES;
	    
	   
	} else {
	    /*
	    * Add this non-newline character
	    */
	    [currentLine addChar:*buffer];
	}
	buffer++;

    }	
    return self;
}

// todo - if standardError and standardOutput are the same, why not
// just forward this message to subprocess:stdoutOutput, which would merge
// stderr and stdout handling (and allow target/action for stderr messages)
-  subprocess:sender stderrOutput:(const char *)buffer
{
    if ( standardError )
	[self sendText:buffer andNewline:NO to:standardError];
    else
	fputs( buffer, stderr );	// To the console.
	return self; // added to remove warning... -- DAY
}

- subprocess:sender done:(int)status :(MiscSubprocessEndCode)code
{
    executionInProgress = NO;
    
    [self finishOutput];
    return self;
}


- (BOOL) runToCompletion { return runToCompletion; }
- setRunToCompletion:(BOOL)c;
{
    runToCompletion = c;
    return self;
}
- (int) sortWhenColumnsMove { return sortWhenColumnsMove; }
- setSortWhenColumnsMove:(int)i
{
    sortWhenColumnsMove = i;
    return self;
}
- (char)delimiter { return delimiter; }
- setDelimiter:(char)c
{
    delimiter = c;
    return self;
}


- setExecArgs:(const char *)a1:(const char *)a2:(const char *)a3
{
    ;	// TODO - finish me
    return self;
}

/*
 * Methods for sending data to the standard input of a process.
 */

/*
 * takeStdinFrom:sender writes [sender stringValue] followed by a newline
 * to the process.
 */
- takeStdinFrom:sender
{
    [self writeToStdin:[sender stringValue]];
    [self writeToStdin:"\n"];
    return self;
}

/*
 * writeToStdin writes a string verbatim to the standard input of the 
 * process.
 */
 
- writeToStdin:(const char *)str
{
    [process send:str withNewline:NO];
    return self;
}


@end
